﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.XPath;

namespace TwinHead.Frameworks.Xml
{
    /// <summary>
    /// XML의 각 엘리먼트(태그)정보를 표현하는 클래스입니다
    /// </summary>
    internal class XmlTag : IXmlTag
    {
        private XPathNavigator navigator;
        private string displayTagName = string.Empty;
        private string tagName = string.Empty;
        private string tagAttribute = string.Empty;
        private string tagValue = string.Empty;
        private bool isRef = false;
        private object tagType = null;
        private IXmlTag parentNode = null;

        private List<IXmlTag> childNodes = new List<IXmlTag>();

        #region IXmlTag 멤버
        /// <summary>
        /// 현재 태그에 상응하는 XML 네비게이터를 설정하거나 가져옵니다.
        /// </summary>
        public XPathNavigator Navigator
        {
            get { return navigator; }
            set { navigator = value; }
        }
        
        /// <summary>
        /// 화면 표시명을 설정하거나 가져옵니다.
        /// </summary>
        public string DisplayTagName
        {
            get { return displayTagName; }
            set { displayTagName = value; }
        }
        
        /// <summary>
        /// 현재 태그의 정규화된 이름을 설정하거나 가져옵니다.
        /// <para>LocalName이 아님에 주의하세요.</para>
        /// </summary>
        public string TagName
        {
            get { return tagName; }
            set { tagName = value; }
        }
        
        /// <summary>
        /// 원본 XML문자열에서 현재 태그에 설정되어 있는 어트리뷰트 문자열을 설정하거나 가져옵니다.
        /// </summary>
        public string TagAttribute
        {
            get { return this.tagAttribute; }
            set { this.tagAttribute = value; }
        }

        /// <summary>
        /// 현재 태그의 값을 설정하거나 반환합니다.
        /// </summary>
        public string TagValue
        {
            get { return tagValue; }
            set { tagValue = value; }
        }
        
        /// <summary>
        /// 현재 태그가 참조 엘리먼트인지 여부를 설정하거나 가져옵니다.
        /// </summary>
        public bool IsRef
        {
            get { return isRef; }
            set { isRef = value; }
        }
        
        /// <summary>
        /// XSD를 변환한 경우 원본 문서에서 기술되어 해석된, 닷넷 프레임워크상의 타입을 설정하거나 반환합니다.
        /// </summary>
        public object TagType
        {
            get { return tagType; }
            set { tagType = value; }
        }
        
        /// <summary>
        /// 현재 태그의 부모태그를 설정하거나 가져옵니다.
        /// </summary>
        public IXmlTag ParentNode
        {
            get { return parentNode; }
            set { parentNode = value; }
        }

        /// <summary>
        /// 현재 태그의 XPATH를 반환합니다.
        /// </summary>
        /// <returns></returns>
        public string GetXPath()
        {
            if (parentNode != null)
                return string.Format( "{0}/{1}", parentNode.GetXPath(), this.TagName );
            return this.TagName;
        }

        /// <summary>
        /// 현재 태그의 하위 태그요소들의 목록을 가져옵니다.
        /// </summary>
        public IList<IXmlTag> ChildNodeCollection
        {
            get { return this.childNodes; }
        }

        /// <summary>
        /// 현재 하위 태그 목록에서 지정한 자식 태그의 인덱스를 반환합니다.
        /// </summary>
        /// <param name="child"></param>
        /// <returns></returns>
        public int IndexOfChildNode( IXmlTag child )
        {
            return childNodes.IndexOf( child );
        }

        /// <summary>
        /// 현재 태그의 하위 태그요소들의 목록의 개수를 가져옵니다.
        /// </summary>
        public int ChildNodesCount
        {
            get { return childNodes.Count; }
        }

        /// <summary>
        /// 지정한 태그명을 가진 태그를 하위 컬렉션에서 찾아 반환합니다.
        /// </summary>
        /// <param name="tagName"></param>
        /// <returns></returns>
        public IList<IXmlTag> FindXmlTag( string tagName )
        {
            List<IXmlTag> lists = new List<IXmlTag>();
            foreach (IXmlTag childTag in childNodes)
            {
                if (childTag.TagName.Equals( TagName, StringComparison.InvariantCultureIgnoreCase ))
                    lists.Add( childTag );

                lists.AddRange( childTag.FindXmlTag( TagName ) );
            }
            return lists;
        }

        /// <summary>
        /// 현재 태그 및 하위 태그들의 구조화된 XML문자열을 반환합니다.
        /// </summary>
        /// <returns></returns>
        public string ToXmlString()
        {
            
            string[] xpath = GetXPath().Split( '/' );
            string indent = string.Empty;
            indent = indent.PadRight( xpath.Length - 1, '\t' );

            if (!string.IsNullOrEmpty( tagAttribute ))
                tagAttribute = " " + tagAttribute.TrimStart();

            StringBuilder sb;

            if (childNodes.Count > 0)
            {
                sb = new StringBuilder(512);
                sb.AppendFormat( "{2}<{0}{1}>", tagName, tagAttribute, indent );

                foreach (IXmlTag childTag in childNodes)
                    sb.AppendFormat( "\r\n{0}", childTag.ToXmlString() );

                sb.AppendFormat( "\r\n{1}</{0}>", tagName, indent );
            }
            else
            {
                sb = new StringBuilder();
                if (string.IsNullOrEmpty( tagValue ))
                    sb.AppendFormat( "{2}<{0}{1}/>", tagName, tagAttribute, indent );
                else
                    sb.AppendFormat( "{3}<{0}{1}>{2}</{0}>", tagName, tagAttribute, NormalizedRefereceENTITY( tagValue ), indent );
            }
            return sb.ToString();
        }

        /// <summary>
        /// XML네비게이터를 기반으로 구조화된 XSD문자열을 반환합니다. (XSD문서를 변환한 경우)
        /// </summary>
        /// <returns></returns>
        public string ToXSDString()
        {
            StringBuilder sb = new StringBuilder(512);
            sb.AppendFormat( "{0}", navigator.OuterXml );
            if (childNodes.Count > 0)
            {
                foreach (IXmlTag childTag in childNodes)
                    sb.AppendFormat( "\r\n{0}", childTag.ToXSDString() );
            }
            return sb.ToString();
        }

        #endregion

        #region 생성자
        /// <summary>
        /// 기본 생성자
        /// </summary>
        internal XmlTag()
        {
        }

        /// <summary>
        /// XML의 각 엘리먼트(태그)정보를 표현하는 클래스의 인스턴스를 생성합니다.
        /// </summary>
        /// <param name="tagName">태그명</param>
        /// <param name="tagValue">태그값</param>
        /// <param name="isRef">현재 태그가 참조 엘리먼트인지 여부</param>
        /// <param name="tagType">태그의 값형식</param>
        /// <param name="parent">부모태그</param>
        public XmlTag( string tagName, string tagValue, bool isRef, object tagType, IXmlTag parent )
        {
            this.tagName = tagName;
            this.tagValue = tagValue;
            this.tagType = tagType;
            this.isRef = isRef;
            this.parentNode = parent;
        }
        /// <summary>
        /// XML의 각 엘리먼트(태그)정보를 표현하는 클래스의 인스턴스를 생성합니다.
        /// </summary>
        /// <param name="tagName">태그명</param>
        /// <param name="tagValue">태그값</param>
        /// <param name="parent">부모태그</param>
        public XmlTag( string tagName, string tagValue, IXmlTag parent )
            : this( tagName, tagValue, false, null, parent )
        {
        }
        /// <summary>
        /// XML의 각 엘리먼트(태그)정보를 표현하는 클래스의 인스턴스를 생성합니다.
        /// </summary>
        /// <param name="tagName">태그명</param>
        /// <param name="tagValue">태그값</param>
        public XmlTag( string tagName, string tagValue )
            : this( tagName, tagValue, false, null, null )
        {
        }

        #endregion


        /// <summary>
        /// 현재 태그의 하위 태그요소들의 목록을 설정하거나 가져옵니다.
        /// </summary>
        internal List<IXmlTag> ChildNodes
        {
            get { return childNodes; }
            set { childNodes = value; }
        }

        private string NormalizedRefereceENTITY( string val )
        {
            val = val.Replace( "&lt;", "<" );
            val = val.Replace( "&gt;", ">" );
            val = val.Replace( "&quot;", "\"" );
            val = val.Replace( "&apos;", "\'" );
            val = val.Replace( "&amp;", "&" );

            val = val.Replace( "&", "&amp;" );
            val = val.Replace( "<", "&lt;" );
            val = val.Replace( ">", "&gt;" );
            val = val.Replace( "\"", "&quot;" );
            val = val.Replace( "\'", "&apos;" );
            return val;
        }

        /// <summary>
        /// 현재 태그의 복사본을 반환합니다.
        /// </summary>
        /// <returns></returns>
        public IXmlTag Clone( IXmlTag parent )
        {
            XmlTag ntag = new XmlTag();
            ntag.DisplayTagName = this.DisplayTagName;
            ntag.IsRef = this.IsRef;

            if (this.navigator != null)
                ntag.Navigator = this.Navigator.Clone();

            ntag.ParentNode = parent;

            ntag.TagAttribute = this.TagAttribute;
            ntag.TagName = this.TagName;
            ntag.TagType = this.TagType;
            ntag.TagValue = this.TagValue;

            if (this.ChildNodes.Count > 0)
            {
                foreach (IXmlTag child in this.ChildNodes)
                    ntag.ChildNodes.Add( child.Clone( ntag ) );
            }
            return ntag;
        }


        #region IDisposable 멤버
        private bool isDisposed = false;
        /// <summary>
        /// 모든 자원을 해제합니다.
        /// </summary>
        public void Dispose()
        {
            this.Dispose( true );
            GC.SuppressFinalize( this );
        }
        /// <summary>
        /// 모든 자원을 해제합니다.
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose( bool disposing )
        {
            if (isDisposed) return;
            if (disposing)
            {
                // 순서가 중요합니다.
                if (childNodes != null)
                {
                    foreach (IXmlTag child in childNodes)
                        child.Dispose();

                    childNodes.Clear();
                    childNodes = null;
                }

                if (tagType != null)
                    tagType = null;

                if (navigator != null)
                    navigator = null;
            }
            isDisposed = true;
        }
        #endregion
    }
}
